// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Author: Markus Gutschke

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "base64.h"
#include "check.h"
#include "http.h"
#include "multipart.h"
#include "net.h"
#include "stringprintf.h"

#define min(a,b) ({ typeof(a) _a=(a); typeof(b) _b=(b); _a < _b ? _a : _b; })


int openHTTPSocket(struct Connection *conn, const char *method,
                   const char *url, const char *header, struct OAuth2 *oauth2,
                   const struct MultiPart *multipart,
                   enum MultiPartEncoding encoding) {
  // Sanity check
  for (const char *ptr = url; *ptr; ++ptr) {
    check(*ptr > ' ');
    check(*ptr < '\x7F');
  }

  // Parse protocol of the URL
  int port = -1, is_ssl;
  char *service = NULL;
  char buf[256];
  if (!memcmp(url, "https://", 8)) {
    url += 8;
    service = "https";
    is_ssl = 1;
  } else if (!memcmp(url, "http://", 7)) {
    url += 7;
    service = "http";
    is_ssl = 0;
  } else {
    check(0);
  }

  // Parse hostname
  size_t hostname_len = strcspn(url, ":/");
  char *hostname;
  check(hostname = malloc(hostname_len+1));
  memcpy(hostname, url, hostname_len);
  hostname[hostname_len] = '\000';

  // Check for an optional port number
  if (url[hostname_len] == ':') {
    port = atoi(url + hostname_len + 1);
    check(port > 0 && port < 65536);
    sprintf(service = buf, "%d", port);
  }

  // Find path of URL
  url = strchr(url, '/');
  if (!url) {
    url = "/";
  }

  // Connect to the given host and port
  int server_fd;
  check((server_fd = openSocket(hostname, service)) >= 0);
  initConnection(conn, server_fd);

  // Open an SSL session, if necessary
  if (is_ssl) {
    upgradeConnection(conn);
  }

  // Send the registration request
  const char *msg = multipart ? serializeMultiPart(multipart, encoding) : NULL;
  char *request = stringPrintf(NULL,
    "%s %s HTTP/1.0\r\n"
    "Host: %s\r\n"
    "User-Agent: Printer In The Sky\r\n"
    "X-CloudPrint-Proxy: PITS\r\n"
    "%s",
    method, url, hostname, header ? header : "");
  free(hostname);

  if (oauth2) {
    // Include OAuth2 authorization, if available
    char *auth = httpHeaderEncoding(oauth2->accessToken);
    request = stringPrintf(request,
      "Authorization: OAuth %s\r\n",
      auth);
    free(auth);
  }
  request = stringPrintf(request, "%s", msg ? msg : "\r\n");
  free((void *)msg);
  size_t reqLen = strlen(request);
  check(connWrite(conn, request, reqLen) == reqLen);
  free(request);
  
  // Read the header and parse the status code
  int status = -1;
  for (ssize_t sz, len = 0, done = -1; done <= 0;) {
    check((sz = connPeek(conn, buf + len, sizeof(buf) - len)) > 0);
    const char *ptr = memrchr(buf + len, '\n', sz);
    if (ptr == NULL) {
      if (len + sz < sizeof(buf)) {
        // The buffer isn't full yet. We still have a chance to find the end
        // of the line. Read the first packet for good and peek into the next
        // network packet.
        check(connRead(conn, buf + len, sz) == sz);
        continue;
      } else {
        // Read a line that is longer than 256 bytes, we don't typically
        // care about these. Discard the trailing part of the line.
        for (;;) {
          check(connRead(conn, buf + len, sz) == sz);
          check((sz = connPeek(conn, buf, sizeof(buf))) > 0);
          ptr = memrchr(buf, '\n', sz);
          if (ptr) {
            len = 0;
            memmove(buf, ptr+1, sz -= (ptr + 1 - buf));
            break;
          }
        }
      }
    }

    // The very first thing we expect to see is the HTTP status code
    // TODO: We should probably be able to handle HTTP redirects
    if (done == -1) {
      done = 0;
      check(!memcmp(buf, "HTTP/", 5));
      char *beg, *end;
      check(beg = memchr(buf, ' ', ptr + 1 - buf));
      for (; beg < ptr && *beg == ' '; ++beg) { }
      check(end = memchr(beg, ' ', ptr + 1 - beg));
      *end = '\000';
      status = atoi(beg);
      check(status > 0 && status <= 1000);
    }

    // Scan header for the first blank line. That marks the end of the header.
    for (char *s = buf, nl = 1; s <= ptr; ++s) {
      char ch = *s;
      if (ch == '\r') {
        // Disregard CR characters.
        continue;
      } else if (ch == '\n') {
        if (nl) {
          // Found a blank line. We are done.
          done = 1;
          ptr  = s;
          sz   = ptr + 1 - buf - len;
        } else {
          // We are starting a new line.
          nl = 1;
        }
      } else {
        // This line is not blank. Keep looking.
        nl = 0;
      }
    }

    // Discard completed lines
    check(connRead(conn, buf + len, sz) == sz);
    len += sz;
    memmove(buf, ptr+1, len -= (ptr + 1 - buf));
  }
  check(status > 0);

  return status;
}

static char *http_request(const char *method, const char *url,
                          struct OAuth2 *oauth2,
                          const struct MultiPart *multipart,
                          enum MultiPartEncoding encoding) {
 auth_refreshed:;
  struct Connection conn = { .fd = -1 };
  int status = openHTTPSocket(&conn, method, url, NULL, oauth2, multipart,
                              encoding);

  // Check status code
  if (oauth2 && status == 403) {
    // Authentication failed. Refresh OAuth2 access token and try again
    destroyConnection(&conn);
    refreshOAuth2(oauth2, NULL);
    goto auth_refreshed;
  }
  check(status == 200 /* HTTP found */);

  // Read response
  char *response = NULL;
  size_t resp_len = 0;
  size_t offset = 0;
  for (;;) {
    check(response = realloc(response, resp_len = offset + 1025));
    ssize_t rc = connRead(&conn, response + offset, resp_len - offset - 1);
    if (rc <= 0) {
      response[offset] = '\000';
      resp_len = offset;
      break;
    }
    offset += rc;
  }

  // Clean up
  destroyConnection(&conn);

  return response;
}

char *GET(const char *url, struct OAuth2 *oauth2) {
  return http_request("GET", url, oauth2, NULL, M_MULTIPART);
}

char *POST(const char *url, struct OAuth2 *oauth2,
           const struct MultiPart *multipart,
           enum MultiPartEncoding encoding) {
  return http_request("POST", url, oauth2, multipart, encoding);
}

char *httpHeaderEncoding(const char *s) {
  char *res;
  ssize_t l = 0;
  for (const char *ptr = s; *ptr; ++ptr, ++l) {
    char ch = *ptr;
    if (l > 75 || ch == '"' || ch < ' ' || ch > '\x7E') {
      // String "s" is potentially not safe to transmit verbatim. Better encode
      // according to RFC 2047.
      static const char beg[] = "\r\n =?utf-8?B?";

      // Compute the total size of the encoded data. We convert to Base64,
      // wrap with "=?utf-8?B?...?=", and wrap lines at 75 characters. If
      // no wrapping is necessary at all, then we skip the initial "\r\n "
      // escaping. This is technically not quite correct, as the caller could
      // append our string to a line that now exceeds 75 characters. In
      // practise, this shouldn't matter.
      l = strlen(s);
      l = (l+2)/3*4;
      l = (l + 75 - sizeof(beg))/(75 - sizeof(beg) + 1)*(sizeof(beg)+1)+l+1;
      if (l - 4 <= 75) {
        l -= 3;
      }
      check(res = malloc(l));
      strcpy(res, beg + (l <= 75 ? 3 : 0));
      char *end = strrchr(res, '\000');

      // Loop over the source string and encode line-by-line of output data.
      while (*s) {
        int raw_len = min(strlen(s), 3*((75-sizeof(beg)+1)/4));
        b64encode(end, s, raw_len);
        s += raw_len;
        end = strrchr(end, '\000');
        strcpy(end, "?=");
        end += 2;
        if (*s) {
          strcpy(end, beg);
          end += sizeof(beg) - 1;
        }
      }

      // Some sanity checks.
      check(end == res + l - 1);
      check(l == strlen(res)+1);
      return res;
    }
  }

  // The string looked pretty harmless. Don't bother encoding or wrapping.
  check(res = strdup(s));
  return res;
}
